// Adapted from https://codegolf.stackexchange.com/a/119563.
export const ord = (i: number) => [, 'st', 'nd', 'rd'][i % 100 >> 3 ^ 1 && i % 10] || 'th';

// charLen adapted from https://mths.be/punycode.
export const byteLen = (str: string) => new TextEncoder().encode(str).length;
export const charLen = (str: string) => {
    let i = 0, len = 0;

    while (i < str.length) {
        const value = str.charCodeAt(i++);

        if (value >= 0xD800 && value <= 0xDBFF && i < str.length) {
            // It's a high surrogate, and there is a next character.
            const extra = str.charCodeAt(i++);

            // Low surrogate.
            if ((extra & 0xFC00) == 0xDC00) {
                len++;
            }
            else {
                // It's an unmatched surrogate; only append this code unit, in
                // case the next code unit is the high surrogate of a
                // surrogate pair.
                len++;
                i--;
            }
        }
        else {
            len++;
        }
    }

    return len;
};

// Frontend version, keep in sync with views/views.go.
export const avatar = (rawURL: string, size: number): URL => {
    const u = new URL(rawURL);

    // Set the avatar size.
    const q = u.searchParams;
    q.set('size', size.toString());
    u.search = q.toString();

    return u;
};

export const strokeLen = (str: string, allowedStrokes: Set<number>) => {
    let len = 0;
    for (const c of str) {
        const codePoint = c.codePointAt(0)!;
        if (codePoint < 128 || allowedStrokes.has(codePoint))
            len += 1;
        // Overlong encodings are illegal in UTF-8 (each code point must be encoded
        // by the shortest possible byte sequence), so each code point corresponds
        // to exactly one UTF-8 byte length, that we can determine as follows.
        else if (codePoint <= 0x07FF)
            len += 2;
        else if (codePoint <= 0xFFFF)
            len += 3;
        else
            len += 4;
    }
    return len;
};

// Small util functions.
/** Assume $ always succeeds and returns an HTMLElement */
export function $<MatchType extends HTMLElement>(selector: string) {
    return document.querySelector(selector) as MatchType;
}
/** Assume $$ returns HTMLElements only */
export function $$<MatchType extends HTMLElement>(selector: string) {
    return document.querySelectorAll(selector) as NodeListOf<MatchType>;
}
export const comma = (i: number | undefined) => i?.toLocaleString('en');

export const amount = (n: number, singular: string, plural?: string) =>
    `${comma(n)} ${n === 1 ? singular : plural ?? singular + 's'}`;

/**
 * Throttle in the following sense:
 *  - never fire more than once in any consecutive `interval` ms
 *  - always fire at least once within the first `interval` ms after the call
 *  - always fire as soon as possible, subject to these restrictions
 */
export function throttle(fn: () => void, interval: number) {
    let pendingTimeout: number | undefined;
    let needsAnother = false;
    return () => {
        if (pendingTimeout) {
            needsAnother = true;
            return;
        }
        else {
            pendingTimeout = setTimeout(() => {
                pendingTimeout = undefined;
                if (needsAnother) fn();
            }, interval);
            fn();
        }
    };
}

/**
 * Fire once when `timeout` ms has passed since the last input.
 */
export function debounce(func: () => void, timeout = 500) {
    let timer: number | undefined;
    return () => {
        clearTimeout(timer);
        timer = setTimeout(func, timeout);
    };
}
